﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.ComponentModel;
using System.Timers;
using P3SS.Settings;
using P3SS.Engine;
using P3SS.FailedMail;
using P3SS.IPC;
using System.Threading;

namespace P3SS.Service
{
	[System.ServiceModel.ServiceBehavior(InstanceContextMode = System.ServiceModel.InstanceContextMode.Single)]
	public class ServiceEngine : IP3SSService
	{
		#region Fields
		private ServiceSettings mySettings;
		private List<String> settingsInProgress;
		private System.Timers.Timer tmrCheckMail;
		private EventLog myEventLog;
		private bool cancellationPending;
		private Dictionary<BackgroundWorker, ManualResetEvent> workersAndEvents;
		private FailedMailEngine myFailedMail;
		private Dictionary<string, SmtpEngine> smtpNames;
		#endregion
		#region Constructors
		public ServiceEngine(EventLog inEventLog)
		{
			tmrCheckMail = new System.Timers.Timer();
			tmrCheckMail.Elapsed +=new ElapsedEventHandler(tmrCheckMail_Elapsed);
			settingsInProgress = new List<string>();
			myEventLog = inEventLog;
			workersAndEvents = new Dictionary<BackgroundWorker, ManualResetEvent>();
			smtpNames = new Dictionary<string, SmtpEngine>();

			loadSettings();
		}
		#endregion
		#region Public Methods
		/// <summary>
		/// Starts the engine
		/// </summary>
		public void Start()
		{
			cancellationPending = false;
			Connector.HostService(this);

			if (mySettings.Accounts.Count > 0)
			{
				tmrCheckMail.Interval = ServiceSettings.MILLISECONDS_PER_MINUTE * mySettings.MinutesBetweenChecks;
				tmrCheckMail.Start();
				checkMail();
			}
		}

		/// <summary>
		/// Stops the engine
		/// </summary>
		public void Stop()
		{
			tmrCheckMail.Stop();
			cancellationPending = true;
			lock (settingsInProgress)
			{
				foreach (Account thisAccount in mySettings.Accounts)
				{
					thisAccount.Pop3Account.StopEngine();
					thisAccount.SmtpAccount.StopEngine();
				}
			}

			WaitHandle[] handles = (from t in workersAndEvents
									select t.Value).ToArray();

			if (handles.Length > 0)
			{
				WaitHandle.WaitAll(handles);
			}

			mySettings.SaveUIDLs();
			FailedMailEngine.SaveFailedMail(myFailedMail);
		}

		/// <summary>
		/// Forces the mail to be checked now
		/// </summary>
		public void CheckMailNow()
		{
			addLogEntry("User requested manual mail collection", MessageType.Information, EventLogCategory.Service);
			checkMail();
		}

		/// <summary>
		/// Creates an error code from given enums
		/// </summary>
		/// <param name="inMessageType">Type of message that was passed (type of event)</param>
		/// <param name="errorCategory">Category of event</param>
		/// <returns></returns>
		public static int makeErrorCode(MessageType inMessageType, EventLogCategory errorCategory)
		{
			return (int)inMessageType * 100 + (int)errorCategory;
		}

		/// <summary>
		/// Converts MessageType into a corresponding Windows Event Log type
		/// </summary>
		/// <param name="inType">Type of Message (MessageType)</param>
		/// <returns>Windows Event Log type</returns>
		public static EventLogEntryType eventlogTypeToEntryType(MessageType inType)
		{
			EventLogEntryType outEntryType = EventLogEntryType.Information;

			switch (inType)
			{
				case MessageType.Information:
					outEntryType = EventLogEntryType.Information;
					break;
				case MessageType.Debug:
					outEntryType = EventLogEntryType.Information;
					break;
				case MessageType.Warning:
					outEntryType = EventLogEntryType.Warning;
					break;
				case MessageType.Error:
					outEntryType = EventLogEntryType.Error;
					break;
			}

			return outEntryType;
		}
		#endregion
		#region Private Methods
		/// <summary>
		/// Adds an item to the Windows Event Log
		/// </summary>
		/// <param name="message">Message for the log</param>
		/// <param name="entryType">Type of entry</param>
		/// <param name="entryCategory">Category of entry</param>
		private void addLogEntry(string message, MessageType entryType, EventLogCategory entryCategory)
		{
			if ((myEventLog != null) && ((entryType != MessageType.Debug) || (mySettings.ShowDebugMessages)))
			{
				myEventLog.WriteEntry(message, eventlogTypeToEntryType(entryType), makeErrorCode(entryType, entryCategory), (short)entryCategory);
			}

			RaiseLogEntryAddedEvent(message, entryType);
		}

		/// <summary>
		/// Loads settings and reconnects event handlers
		/// </summary>
		private void loadSettings()
		{
			mySettings = ServiceSettings.LoadSettings();
			myFailedMail = FailedMailEngine.LoadFailedMail();
			mySettings.LoadUIDLs();
			smtpNames.Clear();

			foreach (Account myAccount in mySettings.Accounts)
			{
				myAccount.Pop3Account.MessageOccurred += new EventHandler<EngineMessageEventArgs>(Pop3Account_MessageOccurred);
				myAccount.SmtpAccount.MessageOccurred += new EventHandler<EngineMessageEventArgs>(SmtpAccount_MessageOccurred);

				string mySmtpName = myAccount.SmtpAccount.ToShortString();
				if (!smtpNames.ContainsKey(mySmtpName))
				{
					smtpNames.Add(mySmtpName, myAccount.SmtpAccount);
				}
			}
		}

		/// <summary>
		/// Main mail checking function. Create BackgroundWorkers and gets them working
		/// </summary>
		private void checkMail()
		{
			addLogEntry("Starting mail collection", MessageType.Information, EventLogCategory.Service);

			loadSettings();

			addLogEntry("Settings loaded", MessageType.Debug, EventLogCategory.Service);

			foreach (Account thisAccount in mySettings.Accounts)
			{
				if (thisAccount.ValidateSettings())
				{
					addLogEntry("Settings ok for account " + thisAccount.ToString(), MessageType.Debug, EventLogCategory.Service);
					bool settingsFree = true;

					lock (settingsInProgress)
					{
						if (settingsInProgress.Contains(thisAccount.ToString()))
						{
							settingsFree = false;
						}
					}

					if (settingsFree)
					{
						addLogEntry("Starting bgworker for account " + thisAccount.ToString(), MessageType.Debug, EventLogCategory.Service);
						BackgroundWorker bgwCheckMailWorker = new BackgroundWorker();
						bgwCheckMailWorker.DoWork += new DoWorkEventHandler(bgwCheckMailWorker_DoWork);
						bgwCheckMailWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgwCheckMailWorker_RunWorkerCompleted);
						bgwCheckMailWorker.RunWorkerAsync(thisAccount);
						bgwCheckMailWorker.WorkerSupportsCancellation = true;

						lock (settingsInProgress)
						{
							settingsInProgress.Add(thisAccount.ToString());
						}

						lock (workersAndEvents)
						{
							workersAndEvents.Add(bgwCheckMailWorker, new ManualResetEvent(false));
						}
					}
					else
					{
						addLogEntry("Timer ticked before account finished for account " + thisAccount.ToString(), MessageType.Warning, EventLogCategory.Service);
					}
				}
				else
				{
					addLogEntry("Settings invalid for account " + thisAccount.ToString(), MessageType.Error, EventLogCategory.Service);
				}
			}
		}

		private void RetryFailedMessages()
		{
			List<FailedMailItem> mailToRetry = myFailedMail.PopAllItems();
			List<FailedMailItem> mailStillFailing = new List<FailedMailItem>();
			int successfulMail = 0;

			while ((!cancellationPending) && (mailToRetry.Count > 0))
			{
				if (smtpNames.ContainsKey(mailToRetry[0].SMTPServer))
				{
					try
					{
						smtpNames[mailToRetry[0].SMTPServer].ResendFailedMail(mailToRetry[0]);
						successfulMail++;
					}
					catch (Exception ex)
					{
						addLogEntry("Account " + mailToRetry[0].SMTPServer + " said:\r\n" + ex.Message, MessageType.Error, EventLogCategory.Smtp);
						mailToRetry[0].FailureReason = ex.ToString();
						mailStillFailing.Add(mailToRetry[0]);
					}
				}
				else
				{
					addLogEntry("Failed mail item " + mailToRetry[0].ToString() + " said:\r\nThe SMTP server \"" + mailToRetry[0].SMTPServer + "\" for this account is missing", MessageType.Error, EventLogCategory.Service);
					mailToRetry[0].FailureReason = "SMTP server for message is missing";
					mailStillFailing.Add(mailToRetry[0]);
				}

				mailToRetry.RemoveAt(0);
			}

			if (cancellationPending)
			{
				foreach (FailedMailItem mailItem in mailToRetry)
				{
					mailStillFailing.Add(mailItem);
				}
			}

			addLogEntry(String.Format("Failed mail retried. {0} successful, {1} still waiting", successfulMail, mailStillFailing.Count), MessageType.Information, EventLogCategory.Service);
			myFailedMail.AddItems(mailStillFailing);
		}
		#endregion
		#region Event Handlers
		void tmrCheckMail_Elapsed(object sender, ElapsedEventArgs e)
		{
			checkMail();
		}

		void bgwCheckMailWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			BackgroundWorker myWorker = (BackgroundWorker)sender;

			mySettings.SaveUIDLs();

			lock (settingsInProgress)
			{
				settingsInProgress.Remove(((Account)e.Result).ToString());
			}

			lock (workersAndEvents)
			{
				workersAndEvents[myWorker].Set();
				workersAndEvents.Remove(myWorker);
			}

			if (settingsInProgress.Count == 0)
			{
				if ((!cancellationPending) && (myFailedMail.MailItems.Count > 0))
				{
					RetryFailedMessages();
				}

				FailedMailEngine.SaveFailedMail(myFailedMail);

				addLogEntry("Mail collection is complete", MessageType.Information, EventLogCategory.Service);
			}
		}

		void bgwCheckMailWorker_DoWork(object sender, DoWorkEventArgs e)
		{
			Account myAccount = (Account)e.Argument;

			List<string> mailMessages = myAccount.Pop3Account.GetAllMail();
			string smtpServer = ((AbstractEngine)myAccount.SmtpAccount).ToString();
			if (cancellationPending)
			{
				foreach (string thisMessage in mailMessages)
				{
					List<string> emailAddresses = myAccount.SmtpAccount.GetToEmailAddresses(thisMessage);
					foreach (string thisAddress in emailAddresses)
					{
						myFailedMail.AddItem(new FailedMailItem { EmailAddress = thisAddress, MailItem = thisMessage, FailureReason = "Service was stopped", SMTPServer = smtpServer });
					}
				}
			}
			else
			{
				if (mailMessages.Count > 0)
				{
					myFailedMail.AddItems(myAccount.SmtpAccount.SubmitAllMail(mailMessages));
				}
			}

			e.Result = myAccount;
		}

		void SmtpAccount_MessageOccurred(object sender, EngineMessageEventArgs e)
		{
			addLogEntry("Account " + sender.ToString() + " said:\r\n" + e.Message, e.TypeOfMessage, EventLogCategory.Smtp);
		}

		void Pop3Account_MessageOccurred(object sender, EngineMessageEventArgs e)
		{
			addLogEntry("Account " + sender.ToString() + " said:\r\n" + e.Message, e.TypeOfMessage, EventLogCategory.Pop3);
		}
		#endregion
		#region Events
		/// <summary>
		/// Occurs when a message is logged to the event log
		/// </summary>
		public event EventHandler<LogEntryEventArgs> LogEntryAdded;
		private void RaiseLogEntryAddedEvent(string inMessage, MessageType inType)
		{
			if (LogEntryAdded != null)
			{
				LogEntryAdded.Invoke(this, new LogEntryEventArgs() { Message = inMessage, Type = inType });
			}
		}
		#endregion
	}

	public class LogEntryEventArgs : EventArgs
	{
		public string Message { get; set; }
		public MessageType Type { get; set; }
	}
}
